Skip to content

08 复合类型类型 - 数组与切片的使用和注意事项 -2

  • 切片(Slice) 是比数组更加灵活和常用的数据结构。
  • 与数组不同,切片的长度是可以动态变化的,它是一种对底层数组的引用,并且比数组更适合在需要处理可变长度的数据时使用。

切片

切片是一个引用类型,可以理解为一个可变长度的数组视图。它有三部分:

  • 指向底层数组的指针(point):切片背后有一个底层数组。
  • 长度(length):切片当前包含的元素数量。
  • 容量(capacity):从切片的起始位置到底层数组的结尾位置的元素总数。

切片的特点:

  1. 动态大小:切片的长度可以根据需要动态增长或缩减。
  2. 引用类型:切片是对底层数组的引用,修改切片的元素会影响到底层数组。
  3. 具有容量(Capacity)和长度(Length):切片除了长度外,还具有容量,表示切片最多可以访问的底层数组的元素数量。

创建切片

声明切片

切片的声明不需要指定长度,只需要指定元素的类型。

go
var slice []int  // 声明一个 int 类型的切片

// 示例:直接声明切片
var slice []int
fmt.Printf("slice = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 输出:slice = [], len = 0, cap = 0

使用 make() 创建切片

使用内置的 make() 函数创建切片,并指定其长度和容量。

go
slice := make([]int, 3, 5)

创建了一个新的切片,并初始化底层数组,它的结构:

  • 3 表示切片的长度,即当前切片中有 3 个元素,这些元素默认是 0(int 的零值)。
  • 5 表示切片的容量,即底层数组的大小为 5。

make([]int, 3, 5) 实际上做了如下事情:

  • 创建一个底层数组,大小为 5(即 [0, 0, 0, 0, 0])。
  • 然后创建一个切片,初始长度为 3,即这个切片只包含底层数组中的前 3 个元素(即 [0, 0, 0]),但切片的容量为 5,这意味着从切片的起点到底层数组的结尾一共有 5 个元素可以使用。
go
slice := make([]int, 5)          // 创建一个长度为 5 的切片,容量也是 5,默认值为 0
slice := make([]int, 5, 10)      // 创建一个长度为 5,容量为 10 的切片

// 示例:通过 make 创建切片
slice := make([]int, 3, 5)
fmt.Printf("slice = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 输出:slice = [0 0 0], len = 3, cap = 5

切片的初始化

切片也可以像数组一样使用字面量进行初始化。

go
slice := []int{1, 2, 3, 4, 5}  // 创建一个长度为 5 的切片

// 示例:通过字面量创建切片
slice := []int{1, 2, 3, 4, 5}
fmt.Printf("slice = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 输出:slice = [1 2 3 4 5], len = 5, cap = 5

通过数组或已有切片创建切片

你可以通过数组或已有的切片生成一个新的切片。

go
slice[start:end]
// start 是起始索引,end 是结束索引(不包括 end 本身)
// 截取范围:[start, end)

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4]  // 创建一个切片,包含 arr 的第 1 到第 3 个元素,即 [2, 3, 4]

// 省略 start 或 end 表示从开头或到结尾
slice := arr[:3]  // 等同于 arr[0:3]
slice := arr[2:]  // 等同于 arr[2:len(arr)]

// 示例:通过数组创建切片
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:3]  // 创建一个切片,包含 arr 的第 1 到第 3 个元素,即 [2, 3]
fmt.Printf("slice = %v, len = %d, cap = %d\n", slice, len(slice), cap(slice))
// 输出:slice = [2 3], len = 2, cap = 4
// 使用 arr[1:3] 创建切片,从 arr[1](包含)到 arr[3](不包含)之间的元素,即两个元素 [2, 3],所以 len 是 2
// 切片的容量是从切片的起始位置(arr[1])到底层数组的末尾(arr[4])的元素数量。也就是说,arr[1] 到 arr[4] 包含 4 个元素 [2, 3, 4, 5],因此 cap 是 4

切片的长度和容量

go
slice := []int{1, 2, 3, 4, 5}

// len() 获取长度
fmt.Println(len(slice))  // 输出:5

// cap() 获取容量
fmt.Println(cap(slice))  // 输出:5

通过截取切片生成的新切片的长度和容量可能会不同。

go
slice := []int{1, 2, 3, 4, 5}
subSlice := slice[1:3]      // [2, 3]
fmt.Println(len(subSlice))  // 输出:2
fmt.Println(cap(subSlice))  // 输出:4(因为底层数组从索引 1 开始后最多有 4 个元素)

使用 append() 动态添加元素

  • append() 函数可以向切片中追加元素。
  • 如果切片的容量不足以容纳新元素,append() 会分配一个新的底层数组,并将原数据复制到新的数组中。
  • append() 可以追加一个切片到另一个切片。
go
slice := []int{1, 2, 3}
slice = append(s, 4, 5)  // 追加 4 和 5
fmt.Println(slice)       // 输出:[1, 2, 3, 4, 5]

// 追加一个切片到另一个切片
slice1 := []int{1, 2, 3}  // len = 3, cap = 3
slice2 := []int{4, 5}
slice1 = append(slice1, slice2...)  // 使用 ... 来解包 slice2,slice1 的 len 变为 5(添加了 slice2 的元素),cap 变为 6(扩容 3*2 = 6)
fmt.Println(slice1)         // 输出:[1, 2, 3, 4, 5]
fmt.Println(len(slice1), cap(slice1))  // 输出:5 6

修改切片

切片是引用类型,修改切片的元素会影响到底层数组,其他共享该数组的切片也会受到影响。

go
slice := []int{1, 2, 3, 4, 5}
subSlice := slice[1:3]    // [2, 3]
subSlice[0] = 100
fmt.Println(slice)        // 输出:[1, 100, 3, 4, 5],底层数组被修改

切片与数组的区别

  1. 长度可变:数组长度固定,而切片长度可变。
  2. 值类型 vs 引用类型:数组是值类型,赋值或传递会产生副本;切片是引用类型,指向同一个底层数组。
  3. 内存使用:切片可以更高效地使用内存,因为它们在操作时不会频繁创建新的数组。

切片拷贝

使用 copy() 函数可以将一个切片的内容复制到另一个切片中。

go
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)        // 将 src 的内容复制到 dst
fmt.Println(dst)      // 输出:[1, 2, 3]

总结

  • 切片是 Go 中常用的数据结构,用于处理动态大小的数据集合。
  • 切片是对底层数组的引用,修改切片会影响到底层数组。
  • append()copy() 是两个常用的切片操作函数,用于动态添加元素和拷贝数据。